import java.io.OutputStream;
import java.io.PrintStream;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.Map;
import java.util.Set;


public class TestRecorder {
	private Map<String, HashMap<Operation, HashMap<DataType, LinkedList<Double>>>> tests;
	private Set<String> techniques;

	enum DataType {
		BOOL,       // 1 bit
		BYTE,       // 8 bit
		SHORT,      // 16 bit
		INTEGER,    // 32 bit
		LONG,       // 64 bit
		FLOAT,      // 32 bit
		DOUBLE,     // 64 bit
		STRING,     // undefined
		COMPOUND    // undefined
	}

	enum Operation {
		WRITE,
		READ,
		RATIO,
		WRITE_ARRAY,
		READ_ARRAY,
		RATIO_ARRAY
	}
	
	public TestRecorder() {
		tests = new HashMap<String, HashMap<Operation, HashMap<DataType, LinkedList<Double>>>>();
		techniques = new LinkedHashSet<String>();

		// hack to guarantee three cols with data
//		techniques.add("Protobuf");
//		techniques.add("Thrift");
//		techniques.add("JAXB");
//		techniques.add("XStream");
	}
	
	public void record(String technique, Operation op, DataType type, double value) {
		if (!techniques.contains(technique))
			techniques.add(technique);
		
		if (!tests.containsKey(technique))
			tests.put(technique, new HashMap<Operation, HashMap<DataType, LinkedList<Double>>>());
		if (!tests.get(technique).containsKey(op))
			tests.get(technique).put(op, new HashMap<DataType, LinkedList<Double>>());

		if (!tests.get(technique).get(op).containsKey(type))
			tests.get(technique).get(op).put(type, new LinkedList<Double>());

		// transform nano to micro seconds
		tests.get(technique).get(op).get(type).add(value);
		
		if (value == 0) {
			System.err.println("Got a value of 0 for " + op + " with " + technique + " on " + type);
		}
	}
	
	public void report(Operation op, OutputStream os, boolean reverse) {
		DecimalFormat df = new DecimalFormat("00000000.0000");
		PrintStream ps = new PrintStream(os);
		ps.print("Type ");

		Operation sortOp = op;
		if (sortOp == Operation.RATIO_ARRAY)
			sortOp = Operation.RATIO;

		if (sortOp == Operation.WRITE_ARRAY)
			sortOp = Operation.READ_ARRAY;

		if (sortOp == Operation.WRITE)
			sortOp = Operation.READ;

		ArrayList<String> sortedTechs = new ArrayList<String>(techniques);     
		Collections.sort(sortedTechs, new TechComparator(sortOp));
		if (reverse) {
			Collections.reverse(sortedTechs);
		}
		
		for (String technique : sortedTechs) {
			ps.print(technique + " ");
		}
		
		//double test = 1.0;
		ps.print("\n");
		for (DataType dataType : DataType.values()) {
			ps.print(dataType + " ");
			for (String technique : sortedTechs) {
				if (!tests.containsKey(technique)) {
					System.err.println("Technique " + technique + " did not record anything!");
					ps.print(df.format(0) + " ");
				} else if (!tests.get(technique).containsKey(op)) {
					System.err.println("Test " + technique + " did not record " + op);
					ps.print(df.format(0) + " ");
				} else if (!tests.get(technique).get(op).containsKey(dataType)) {
					System.err.println("Test " + technique + " did not record " + op + " for type " + dataType);
					ps.print(df.format(0) + " ");
				} else {
					double value = avg(tests.get(technique).get(op).get(dataType));
					if (op != Operation.RATIO && op != Operation.RATIO_ARRAY)
						value /= 1000;
					
					ps.print(df.format(value) + " ");
					//ps.print(df.format(test++) + " ");
				}
			}
			ps.print("\n");
		}
	}
	
	private static double avg(LinkedList<Double> list) {
		double val = 0;
		for (double d : list) {
			if (d == 0) {
				// something went wrong
				return 0;
			}
			val += d / (double)list.size();
		}
		return val;
	}
	
	private class TechComparator implements Comparator<String> {
		public Operation op;
		
		public TechComparator(Operation op) {
			this.op = op;
		}
		
		@Override
		public int compare(String t1, String t2) {
			if (!tests.containsKey(t1) || !tests.containsKey(t2))
				return 0;
			if (!tests.get(t1).containsKey(op) || !tests.get(t2).containsKey(op))
				return 0;
			// find the first datatype both recorded and compare
			for (DataType type : DataType.values()) {
				if (tests.get(t1).get(op).containsKey(type) && tests.get(t2).get(op).containsKey(type)) {
					double rec1 = avg(tests.get(t1).get(op).get(type));
					double rec2 = avg(tests.get(t2).get(op).get(type));
					return Double.compare(rec1, rec2);
				}
			}
			
			return 0;
		}

	}

}
